package com.redijedi.tapestry.components;

import java.util.List;

import org.apache.tapestry.Asset;
import org.apache.tapestry.MarkupWriter;
import org.apache.tapestry.ValueEncoder;
import org.apache.tapestry.annotations.Component;
import org.apache.tapestry.annotations.Inject;
import org.apache.tapestry.annotations.Parameter;
import org.apache.tapestry.annotations.Path;
import org.apache.tapestry.annotations.SetupRender;
import org.apache.tapestry.corelib.base.AbstractField;
import org.apache.tapestry.corelib.components.Label;
import org.apache.tapestry.corelib.components.Loop;
import org.apache.tapestry.corelib.components.Radio;
import org.apache.tapestry.corelib.components.RadioGroup;
import org.apache.tapestry.dom.Element;
import org.apache.tapestry.services.Environment;
import org.apache.tapestry.services.FormSupport;

import com.redijedi.tapestry.internal.EnvironmentData;
import com.redijedi.tapestry.internal.InternalUtils;
import com.redijedi.tapestry.internal.LongValueEncoder;
import com.redijedi.tapestry.internal.StringValueEncoder;

/**
 * This component provides the ability to associate a RadioGroup and its
 * subordinate Radio fields with a set of values displayed as a rating scale.
 * This is typified by the "star field" where grayed stars represent the choices
 * and highlighted stars represent the chosen value and all values up to the
 * chosen value from left to right.
 * <p>
 * This is in fact that default visual appearance. However, the images can be
 * overridden via parameters and the entire component can, of course, be styled
 * via CSS.
 * <p>
 * As an added benefit, since the underlying representation is simply a
 * RadioGroup with Radio fields it should degrade well when JS and/or CSS is
 * disabled. This should keep the component rather accessible.
 * 
 * @author torr
 * 
 */
public class RatingField extends AbstractField {

	@Parameter(value = "prop:componentResources.id", defaultPrefix = "literal")
	private String _id;

	@Parameter(required = true)
	private Object _value;

	@Parameter(required = true)
	private Object _source;

	private List<? extends Object> _listSource;

	@Parameter
	private ValueEncoder<?> _encoder;

	@Parameter(required = false)
	private Asset _selectedImage;

	@Parameter(required = false)
	private Asset _unselectedImage;

	@Inject
	private Environment _environment;

	@Inject
	@Path("rating.css")
	private Asset _styleSheet;

	@Inject
	@Path("rating_default_selected.gif")
	private Asset _defaultSelectedImage;

	@Inject
	@Path("rating_default_unselected.gif")
	private Asset _defaultUnselectedImage;

	@SuppressWarnings("unused")
	@Component(parameters = { "value=inherit:value", "encoder=encoder" })
	private RadioGroup _radioGroup;

	@SuppressWarnings("unused")
	@Component(parameters = { "source=prop:source", "value=loopValue" })
	private Loop _loop;

	private Object _loopValue;

	@SuppressWarnings("unused")
	@Component(parameters = { "value=loopValue", "label=prop:radioLabel" })
	private Radio _radio;

	@SuppressWarnings("unused")
	@Component(parameters = { "for=radio" })
	private Label _label;

	@Inject
	@Path("rating.js")
	private Asset _ratingScript;

	/**
	 * Returns the component's ID.
	 * 
	 * @return
	 */
	public String getId() {
		return _id;
	}

	/**
	 * Returns the image representing an unselected value.
	 * 
	 * @return
	 */
	public Asset getUnselectedImage() {
		if (_unselectedImage == null) {
			return _defaultUnselectedImage;
		} else {
			return _unselectedImage;
		}
	}

	/**
	 * Returns the image representing a selected value.
	 * 
	 * @return
	 */
	public Asset getSelectedImage() {
		if (_selectedImage == null) {
			return _defaultSelectedImage;
		} else {
			return _selectedImage;
		}
	}

	/**
	 * Returns an appropriate ValueEncoder implementation based on the value
	 * type.
	 * 
	 * @return
	 */
	public ValueEncoder<?> getEncoder() {

		if (_encoder != null) {
			return _encoder;
		}

		if (_value instanceof Long) {
			return new LongValueEncoder();
		} else {
			return new StringValueEncoder();
		}
	}

	/**
	 * Returns a reasonable label for the radio value. If the value is primitive
	 * it will be returned as is. Otherwise the toString() method will be called
	 * on the value object.
	 * 
	 * @return
	 */
	public String getRadioLabel() {
		return _loopValue.toString();
	}

	/**
	 * Getter for the loop iteration's value.
	 * 
	 * @return
	 */
	public Object getLoopValue() {
		return _loopValue;
	}

	/**
	 * Setter for the loop iteration's value.
	 */
	public void setLoopValue(Object loopValue) {
		_loopValue = loopValue;
	}

	/**
	 * Returns an Iterable implementation of the source provided.
	 * 
	 * @return
	 */
	public List<? extends Object> getSource() {
		return _listSource;
	}

	/*
	 * Event listeners
	 */

	/**
	 * Manages adding the correct JS includes into the page head section as well
	 * as initializing the source, etc.
	 */
	@SetupRender
	void setupComponent(MarkupWriter writer) {
		if (!getIsHeadWritten()) {
			Element head = writer.getDocument().find("html/head");
			head.element("script", "type", "text/javascript", "src",
					_ratingScript.toClientURL());
			head.element("link", "type", "text/css", "href", _styleSheet
					.toClientURL(), "rel", "stylesheet");
			setIsHeadWritten(true);
		}

		if (_source instanceof String) {
			String stringSource = (String) _source;
			_listSource = InternalUtils
					.convertStringToListOfStrings(stringSource);
		}
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see org.apache.tapestry.corelib.base.AbstractField#processSubmission(org.apache.tapestry.services.FormSupport,
	 *      java.lang.String)
	 */
	@Override
	protected void processSubmission(FormSupport arg0, String arg1) {
		// TODO Auto-generated method stub

	}

	@SuppressWarnings("unchecked")
	private boolean getIsHeadWritten() {
		// see if the head content has been set yet
		EnvironmentData data = _environment.peek(EnvironmentData.class);
		if (data == null) {
			return false;
		}
		if (data.containsKey("isRatingHeadSet")) {
			return (Boolean) data.get("isRatingHeadSet");
		} else {
			return false;
		}
	}

	@SuppressWarnings("unchecked")
	private void setIsHeadWritten(Boolean written) {
		// see if the head content has been set yet
		EnvironmentData data = _environment.peek(EnvironmentData.class);
		if (data == null) {
			data = new EnvironmentData();
		}
		data.put("isRatingHeadSet", written);
		_environment.push(EnvironmentData.class, data);
	}

}
